﻿using System;
using System.Net;
using System.Windows;
using System.Net.Sockets;
using System.Threading;
using System.Text;
using System.Diagnostics;
using System.Collections.Generic;
using System.IO;

namespace UPnP
{
    public class Scanner
    {
        const string multicastIP = "239.255.255.250";
        const int multicastPort = 1900;
        const int unicastPort = 1901;
        const int MaxResultSize = 8000;

        Socket socket;
        Timer timer;
        Action<string> FinderCallback;
        int SendCount;
        SocketAsyncEventArgs sendEvent;
        bool SocketClosed;

        public void FindDevices(string whatToFind, int seconds, Action<string> FoundCallback)
        {
            if (seconds < 1 || seconds > 4)
                throw new ArgumentOutOfRangeException();

            this.FinderCallback = FoundCallback;

            string find = "M-SEARCH * HTTP/1.1\r\n" +
               "HOST:239.255.255.250:1900\r\n" +
               "MAN:\"ssdp:discover\"\r\n" +
               "MX:" + seconds.ToString() + "\r\n" +
               "ST:" + whatToFind + "\r\n" +
               "\r\n";

            socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
            byte[] MulticastData = Encoding.UTF8.GetBytes(find);
            socket.SendBufferSize = MulticastData.Length;
            sendEvent = new SocketAsyncEventArgs();
            sendEvent.RemoteEndPoint = new IPEndPoint(IPAddress.Parse(multicastIP), multicastPort);
            sendEvent.SetBuffer(MulticastData, 0, MulticastData.Length);
            sendEvent.Completed += new EventHandler<SocketAsyncEventArgs>(sendEvent_Completed);

            // Set a one-shot timer for the Search time plus a second
            TimerCallback cb = new TimerCallback((state) =>
            {
                Debug.WriteLine("Timer fired, closing socket");
                SocketClosed = true;
                socket.Close();
            });
            timer = new Timer(cb, null, TimeSpan.FromSeconds(seconds + 1), new TimeSpan(-1));

            // Kick off the initial Send
            SendCount = 3;
            SocketClosed = false;
            socket.SendToAsync(sendEvent);
        }

        void sendEvent_Completed(object sender, SocketAsyncEventArgs e)
        {
            if (e.SocketError != SocketError.Success)
            {
                Debug.WriteLine("Socket error {0}", e.SocketError);
                FinderCallback(null);
            }
            else
            {
                if (e.LastOperation == SocketAsyncOperation.SendTo)
                {
                    if (--SendCount != 0)
                    {
                        if (!SocketClosed)
                            socket.SendToAsync(sendEvent);
                    }
                    else
                    {
                        // When the initial multicast is done, get ready to receive responses
                        e.RemoteEndPoint = new IPEndPoint(IPAddress.Any, unicastPort);
                        socket.ReceiveBufferSize = MaxResultSize;
                        byte[] receiveBuffer = new byte[MaxResultSize];
                        e.SetBuffer(receiveBuffer, 0, MaxResultSize);
                        socket.ReceiveFromAsync(e);
                    }
                }
                else if (e.LastOperation == SocketAsyncOperation.ReceiveFrom)
                {
                    // Got a response, so decode it
                    string result = Encoding.UTF8.GetString(e.Buffer, 0, e.BytesTransferred);
                    if (result.StartsWith("HTTP/1.1 200 OK"))
                    {
                        FinderCallback(result);
                    }
                    else
                    {
                        Debug.WriteLine("INVALID SEARCH RESPONSE");
                    }

                    if (!SocketClosed)
                    {
                        // and kick off another read
                        socket.ReceiveFromAsync(e);
                    }
                    else
                    {
                        // unless socket was closed, when declare the scan is complete
                        FinderCallback(null);
                    }
                }
            }
        }

        // SSDP response handler, returns location of xml file, or null
        public static string GetSSDPLocation(string response)
        {
            var dict = ParseSSDPResponse(response);
            if (dict != null && dict.ContainsKey("location"))
                return dict["location"];
            else
                return null;
        }

        // Probably not exactly compliant with RFC 2616 but good enough for now
        private static Dictionary<string, string> ParseSSDPResponse(string response)
        {
            StringReader reader = new StringReader(response);

            string line = reader.ReadLine();
            if (line != "HTTP/1.1 200 OK")
                return null;

            Dictionary<string, string> result = new Dictionary<string, string>();

            for (; ; )
            {
                line = reader.ReadLine();
                if (line == null)
                    break;
                if (line != "")
                {
                    int colon = line.IndexOf(':');
                    if (colon < 1)
                    {
                        return null;
                    }
                    string name = line.Substring(0, colon).Trim();
                    string value = line.Substring(colon + 1).Trim();
                    if (string.IsNullOrEmpty(name))
                    {
                        return null;
                    }
                    result[name.ToLowerInvariant()] = value;
                }
            }
            return result;
        }
    }
}
